// Copyright 2018 Palantir Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package common

import (
	"context"
	"regexp"
	"sort"
	"testing"
	"time"

	"github.com/palantir/policy-bot/pull"
	"github.com/palantir/policy-bot/pull/pulltest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestCandidates(t *testing.T) {
	now := time.Now()

	ctx := context.Background()
	prctx := &pulltest.Context{
		CommentsValue: []*pull.Comment{
			{
				CreatedAt: now.Add(0 * time.Minute),
				Body:      "I like to comment!",
				Author:    "rrandom",
			},
			{
				CreatedAt: now.Add(2 * time.Minute),
				Body:      "Looks good to me :+1:",
				Author:    "mhaypenny",
			},
			{
				CreatedAt: now.Add(4 * time.Minute),
				Body:      ":lgtm:",
				Author:    "ttest",
			},
			{
				CreatedAt: now.Add(8 * time.Minute),
				Body:      "I approve this, because it looks good to me.",
				Author:    "wstrawmoney",
			},
		},
		ReviewsValue: []*pull.Review{
			{
				CreatedAt: now.Add(1 * time.Minute),
				Author:    "rrandom",
				State:     pull.ReviewCommented,
			},
			{
				CreatedAt: now.Add(3 * time.Minute),
				Author:    "mhaypenny",
				State:     pull.ReviewChangesRequested,
				Body:      "pr needs work",
			},
			{
				CreatedAt: now.Add(5 * time.Minute),
				Author:    "ttest",
				State:     pull.ReviewApproved,
			},
			{
				CreatedAt: now.Add(7 * time.Minute),
				Author:    "santaclaus",
				Body:      "nice",
				State:     pull.ReviewApproved,
			},
			{
				CreatedAt: now.Add(9 * time.Minute),
				Author:    "dasherdancer",
				Body:      "nIcE",
				State:     pull.ReviewApproved,
			},
		},
	}

	t.Run("comments", func(t *testing.T) {
		m := &Methods{
			Comments: []string{":+1:", ":lgtm:"},
		}

		cs, err := m.Candidates(ctx, prctx)
		require.NoError(t, err)

		sort.Sort(CandidatesByCreationTime(cs))

		require.Len(t, cs, 2, "incorrect number of candidates found")
		assert.Equal(t, "mhaypenny", cs[0].User)
		assert.Equal(t, "ttest", cs[1].User)
	})

	t.Run("commentPatterns", func(t *testing.T) {
		m := &Methods{
			CommentPatterns: []Regexp{
				NewCompiledRegexp(regexp.MustCompile("^(?i:looks good to me)")),
			},
		}

		cs, err := m.Candidates(ctx, prctx)
		require.NoError(t, err)

		sort.Sort(CandidatesByCreationTime(cs))

		require.Len(t, cs, 1, "incorrect number of candidates found")
		assert.Equal(t, "mhaypenny", cs[0].User)
	})

	t.Run("githubReviewCommentPatterns", func(t *testing.T) {
		githubReview := true
		m := &Methods{
			GithubReview:      &githubReview,
			GithubReviewState: pull.ReviewApproved,
			GithubReviewCommentPatterns: []Regexp{
				NewCompiledRegexp(regexp.MustCompile("(?i)nice")),
			},
		}

		cs, err := m.Candidates(ctx, prctx)
		require.NoError(t, err)

		sort.Sort(CandidatesByCreationTime(cs))

		require.Len(t, cs, 2, "incorrect number of candidates found")
		assert.Equal(t, "santaclaus", cs[0].User)
		assert.Equal(t, "dasherdancer", cs[1].User)
	})

	t.Run("reviews", func(t *testing.T) {
		githubReview := true
		m := &Methods{
			GithubReview:      &githubReview,
			GithubReviewState: pull.ReviewChangesRequested,
		}

		cs, err := m.Candidates(ctx, prctx)
		require.NoError(t, err)

		sort.Sort(CandidatesByCreationTime(cs))

		require.Len(t, cs, 1, "incorrect number of candidates found")
		assert.Equal(t, "mhaypenny", cs[0].User)
	})

	t.Run("deduplicate", func(t *testing.T) {
		githubReview := true
		m := &Methods{
			Comments:          []string{":+1:", ":lgtm:"},
			GithubReview:      &githubReview,
			GithubReviewState: pull.ReviewApproved,
		}

		cs, err := m.Candidates(ctx, prctx)
		require.NoError(t, err)

		sort.Sort(CandidatesByCreationTime(cs))

		require.Len(t, cs, 4, "incorrect number of candidates found")
		assert.Equal(t, "mhaypenny", cs[0].User)
		assert.Equal(t, "ttest", cs[1].User)
		assert.Equal(t, "santaclaus", cs[2].User)
		assert.Equal(t, "dasherdancer", cs[3].User)
	})
}

func TestCandidatesByCreationTime(t *testing.T) {
	cs := []*Candidate{
		{
			User:      "c",
			CreatedAt: time.Date(2018, 6, 29, 12, 0, 0, 0, time.UTC),
		},
		{
			User:      "a",
			CreatedAt: time.Date(2018, 6, 28, 0, 0, 0, 0, time.UTC),
		},
		{
			User:      "d",
			CreatedAt: time.Date(2018, 6, 29, 14, 0, 0, 0, time.UTC),
		},
		{
			User:      "b",
			CreatedAt: time.Date(2018, 6, 29, 10, 0, 0, 0, time.UTC),
		},
	}

	sort.Sort(CandidatesByCreationTime(cs))

	for i, u := range []string{"a", "b", "c", "d"} {
		assert.Equalf(t, u, cs[i].User, "candidate at position %d is incorrect", i)
	}
}

func TestMethodsFields(t *testing.T) {
	pattern := NewCompiledRegexp(regexp.MustCompile("^(?i:LGTM)$"))

	fields := map[string]struct {
		Seed         Methods
		Get          func(Methods) any
		UnsetValue   any
		SetValue     any
		DefaultValue any // use SetValue if nil
	}{
		"Comments": {
			Seed:       Methods{Comments: []string{"+1"}},
			Get:        func(m Methods) any { return m.GetComments() },
			UnsetValue: []string(nil),
			SetValue:   []string{"+1"},
		},
		"CommentPatterns": {
			Seed:       Methods{CommentPatterns: []Regexp{pattern}},
			Get:        func(m Methods) any { return m.GetCommentPatterns() },
			UnsetValue: []Regexp(nil),
			SetValue:   []Regexp{pattern},
		},
		"GithubReview": {
			Seed:       Methods{GithubReview: ptr(true)},
			Get:        func(m Methods) any { return m.IsGithubReview() },
			UnsetValue: false,
			SetValue:   true,
		},
		"GithubReviewCommentPatterns": {
			Seed:       Methods{GithubReviewCommentPatterns: []Regexp{pattern}},
			Get:        func(m Methods) any { return m.GetGithubReviewCommentPatterns() },
			UnsetValue: []Regexp(nil),
			SetValue:   []Regexp{pattern},
		},
		"BodyPatterns": {
			Seed:       Methods{BodyPatterns: []Regexp{pattern}},
			Get:        func(m Methods) any { return m.GetBodyPatterns() },
			UnsetValue: []Regexp(nil),
			SetValue:   []Regexp{pattern},
		},
	}

	for field, spec := range fields {
		t.Run(field, func(t *testing.T) {
			v := spec.Get(Methods{})
			assert.Equal(t, spec.UnsetValue, v, "incorrect unset value")

			v = spec.Get(spec.Seed)
			assert.Equal(t, spec.SetValue, v, "incorrect set value")

			v = spec.Get(Methods{Defaults: &spec.Seed})
			if spec.DefaultValue != nil {
				assert.Equal(t, spec.DefaultValue, v, "incorrect default value")
			} else {
				assert.Equal(t, spec.SetValue, v, "incorrect default value")
			}
		})
	}
}

func ptr[T any](v T) *T {
	return &v
}
